// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rbchat.network.http.bigfile;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;

import com.eva.epc.common.util.CommonUtils;
import com.x52im.rbchat.MyApplication;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;

import java.io.File;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;

import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;  
  
/** 
 * 大文件上传任务封装实现类(支持大文件断点分块上传)。
 * 
 * <pre>
 * 【大文件上传的技术难点】：
 * 因为标准的http协议中并未包含文件的断点上传，这也就意味着各主流http库（比如android端的okhttp、
 * 服务端的apache fileupload库等）都不能原生支持断点上传（即指定字节索引位置的文件数据上传），非
 * 得让它们支持那就得直接改它们的源码了，这样无论是日后的升级、维护还是更换方案，代价都太大了。
 * 
 * 【本类的大文件实现思路】：
 * 1）客户端将文件分成块，按块逐块上传到服务端；
 * 2）服务端先将各块临时保存；
 * 3）服务端判定所有块都上传完成后，将这些块临时文件拼合成正式的文件（并删除临时文件）。
 * 
 * 【本类的大文件实现特色】：
 * 1）技术原理简单易行：不需要改任何http标准通用类代码，直接就用；
 * 2）实际应用稳定可靠：在网络不好的情况下，如果需要断点上传，只需要从上次上传完成的最后一块的前推一块上传（前推
 *    一块上传的目的是怕最后一块的数据因上次任务中断而不完整），技术上很经济。
 * </pre>
 * 
 * @author hst, Jack Jiang
 * @since 4.3
 */  
public class BigFileUploadTask implements Runnable
{
    private final static String TAG = BigFileUploadTask.class.getSimpleName();
    
    /** 较大文件的分块大小：1M */
    public final static int FILE_BLOCK_LENGTH_BIGFILE = 1024 * 1024;
    /** 较小文件的分块大小：250KB */
    public final static int FILE_BLOCK_LENGTH_SMALLFILE = 250 * 1024;
    

    public final static int UPLOAD_STATUS_INIT = 0;
    public final static int UPLOAD_STATUS_UPLOADING = 1;
    public final static int UPLOAD_STATUS_PAUSE = 2;
    public final static int UPLOAD_STATUS_SUCCESS = 3;
    public final static int UPLOAD_STATUS_ERROR = -1;
    
//    private static String FILE_MODE = "rwd";  
    private OkHttpClient mClient;  
//    private SQLiteDatabase db;  
    private BigFileUploadTaskListener mListener;  
  
    private Builder mBuilder;
    
    // task id  
    private String id;
    // file upload url  
    private String url;
    // File name when saving  
    private String fileName;
    // 文件数据绝对路径
    private String filePath;
    // 总文件的md5码
    private String fileMd5;
    // 用户要额外上传的参数
    private HashMap<String, String> userPropeties;
    
    // 上传状态
    private int uploadStatus;
    // 当前是第几块（注意：基数是从1开始）  
    private int chunck;
    // 文件分块的总块数
    private int chuncks;
    
    private int errorCode;  
    
    static String BOUNDARY = "----------" + System.currentTimeMillis();  
    
    public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("multipart/form-data;boundary=" + BOUNDARY);
  
    private BigFileUploadTask(Builder builder) {  
        mBuilder = builder;  
        mClient = new OkHttpClient();  
        
        this.id = mBuilder.id;  
        this.url = mBuilder.url;  
        this.fileName = mBuilder.fileName;  
        this.filePath = mBuilder.filePath;
        this.fileMd5 = mBuilder.fileMd5;
        this.userPropeties = mBuilder.userPropeties;
        
        this.uploadStatus = mBuilder.uploadStatus;  
        this.chunck = mBuilder.chunck;  
        
        this.setmListener(mBuilder.listener);
    }  
  
    @Override  
    public void run() {  
        try {
            
            // 参数合法性检查
            if(CommonUtils.isStringEmpty(id, true)
                    || CommonUtils.isStringEmpty(url, true)
                    || CommonUtils.isStringEmpty(fileName, true)
                    || CommonUtils.isStringEmpty(filePath, true)
                    || CommonUtils.isStringEmpty(fileMd5, true)
                    || this.chunck < 1)
            {

                String logStr = "[id=" + id + ", url=" + url + ", fileName=" + fileName + ", filePath=" + filePath + ", fileMd5=" + fileMd5 + ", chunck=" + chunck + "]";
                Log.i(TAG, "【大文件上传】各参数值：" + logStr);
                throw new IllegalArgumentException("【大文件上传】无效的参数：" + logStr);
            }
            
//            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator +fileName);
            File file = new File(this.filePath);
            
            if(file.length() <= 0){
                throw new IllegalArgumentException("无效的文件大小：fileName=" +fileName+", filePath="+filePath+", id="+id);
            }
            
            // 当文件大小大于2M时，按大文件的分块大小处理，否则按小文件的分块大小处理
            int blockLength = (file.length() > 2 * 1024 * 1024 ? FILE_BLOCK_LENGTH_BIGFILE : FILE_BLOCK_LENGTH_SMALLFILE);

            if (file.length() % blockLength == 0) {  
                chuncks = (int) file.length() / blockLength;  
            } else {  
                chuncks = (int) file.length() / blockLength + 1;
            }

            Log.i(TAG, "【大文件上传】本文要上传的文件："+filePath
                    +"，文件大小："+file.length()+", 分块数："+chuncks+", 默认每块大小："+blockLength);


            boolean uploadAllComplete = true;
            
            while (chunck <= chuncks
                    && uploadStatus != UPLOAD_STATUS_PAUSE
                    && uploadStatus != UPLOAD_STATUS_ERROR)  
            {
                uploadStatus = UPLOAD_STATUS_UPLOADING;

                RosterElementEntity localUserInfo = MyApplication.getInstance2().getIMClientManager().getLocalUserInfo();

                
                //** 附加上要上传的额外参数
                Map<String, String> params = new HashMap<String, String>();  
                params.put("name", fileName);  
                params.put("chunks", chuncks + "");  
                params.put("chunk", chunck + "");
                params.put("totalLength", file.length()+"");
                
                if(fileMd5 != null && fileMd5.length() > 0)
                    params.put("totalFileMd5", fileMd5);
                
                if(localUserInfo != null) {
                    // 默认带上token，用于服务端的安全检查，以便排除掉非法请求
//                  params.put("token", localUserInfo.getToken());
                    params.put("user_uid", localUserInfo.getUser_uid());
                }

                
                // 用户附加的额外参数
                if(this.userPropeties != null) {
                    for (String key : this.userPropeties.keySet())
                        params.put(key, this.userPropeties.get(key));
                }

                final byte[] mBlock = readBlockFromFile((chunck - 1) * blockLength, file, blockLength); 
                // 没有读取到块数据，结束本次余下块的上传任务
                if(mBlock == null){
                    // 任何一个分块上传失败则直接停止后续分块的继续上传
                    uploadAllComplete = false;
                    break;
                }
                
                MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);  
                addParams(builder, params);

                //** 要上传的文件块数据
                RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, mBlock);  
                builder.addFormDataPart("mFile", fileName, requestBody);  
                
                Request request = new Request.Builder()
                        // v10.0版开始，为了规范文件上传token传递规范，目前取消了通过formData传自定义token字段，改为使用RFC标准的Authorization http头形式传输
                        .header("Authorization", MyApplication.getInstance2().getIMClientManager().getLocalUserInfo().getToken())
                        .url(url)//+ "uploaderWithContinuinglyTransferring")  
                        .post(builder.build())  
                        .build();

                // 使用的是okhttp3的同步上传方式，但整个Task会在BigFileUploadManager中的多线程中被调用
                Response response = mClient.newCall(request).execute();

                final boolean ok = response.isSuccessful();
                final String result = response.body().string();
                
                if (ok) {
                    Log.i(TAG, "【大文件上传】大文件第"+chunck+"\"块\"上传成功完成。(code="+response.code()+",result="+result+")");

                    // 此处直接将百分比传过去，而不是在Hadler里实时取百分比，因为handler
                    // 处于不同的线程中，等到handler取到时都走到下一步即chunck++完成了，
                    // 这样就会导致handler实时取到的进度比实际chunck多1
                    onCallBack(getDownLoadPercent());
                    chunck++;  
                   /* if (chunck <= chuncks) { 
                         run(); 
                    }*/  
                } else {
                    Log.w(TAG, "【大文件上传】大文件上传失败：(code="+response.code()+",result="+result+")");
                    
                    uploadStatus = UPLOAD_STATUS_ERROR;
                    errorCode = response.code();
                    
                    onCallBack(getDownLoadPercent());
                    
                    // 任何一个分块上传失败则直接停止后续分块的继续上传
                    uploadAllComplete = false;
                    break; 
                }
            }  
            
            if(uploadAllComplete){
                Log.i(TAG, "【大文件上传】【！成功完成】文件"+fileName+"上传完成，chunck="+(chunck-1)+", chuncks="+chuncks+", uploadStatus="+uploadStatus);
                
                uploadStatus = UPLOAD_STATUS_SUCCESS;
                onCallBack(getDownLoadPercent());
            } else{
                Log.e(TAG, "【大文件上传】【！失败中断】文件"+fileName+"上传失败，chunck="+(chunck-1)+", chuncks="+chuncks+", uploadStatus="+uploadStatus);
                uploadStatus = UPLOAD_STATUS_ERROR;
                onCallBack(getDownLoadPercent());
            }
        } catch (Exception e) {
            Log.w(TAG, "【大文件上传】大文件上传中出错了，原因："+e.getMessage(), e);
            uploadStatus = UPLOAD_STATUS_ERROR;  
            onCallBack(getDownLoadPercent());
            Log.e(TAG, e.getMessage(), e);
        }  
    }  
  
  
/*    *//** 
     * 删除数据库文件和已经上传的文件 
     *//* 
    public void cancel() { 
        if (mListener != null) 
            mListener.onCancel(BigFileUploadTask.this); 
    }*/  
  
    /** 
     * 分发回调事件到ui层 
     */  
    private void onCallBack(int percent) {
        Message msg = new Message();
        msg.what = uploadStatus;
        msg.arg1 = percent;
        mHandler.sendMessage(msg);
        // 同步manager中的task信息  
        //BigFileUploadManager.getInstance().updateUploadTask(this);  
    }  
  
    Handler mHandler = new Handler(Looper.getMainLooper()) {  
        @Override  
        public void handleMessage(Message msg) {  
            int code = msg.what;
            int percent = msg.arg1;
            switch (code) {  
                // 上传失败  
                case UPLOAD_STATUS_ERROR:  
//                    mListener.onError(BigFileUploadTask.this, errorCode);  
                    mListener.onError(fileName, fileMd5, filePath, errorCode, chunck, chuncks);
                    break;  
                // 正在上传  
                case UPLOAD_STATUS_UPLOADING:  
//                    mListener.onUploading(BigFileUploadTask.this, getDownLoadPercent());  
                    mListener.onUploading(fileName, fileMd5, filePath
                            , percent//, getDownLoadPercent()
                            , chunck, chuncks);
                    break;
                // 暂停上传  
                case UPLOAD_STATUS_PAUSE:  
//                    mListener.onPause(BigFileUploadTask.this);  
                    mListener.onPause(fileName, fileMd5, filePath, chunck, chuncks);
                    break;
                // 暂停上传  
                case UPLOAD_STATUS_SUCCESS:
                    mListener.onUploadSuccess(fileName, fileMd5, filePath, chunck, chuncks);
                    break;

            }  
        }  
    };

    /**
     * 获得当前下载百分比。
     *
     * @return 0~100的进度值
     */
    private int getDownLoadPercent() {  
        int baifenbi = 0;// 接受百分比的值  
        if (chunck >= chuncks) {  
            return 100;  
        }  
        double baiy = chunck * 1.0;  //
        double baiz = chuncks * 1.0;  
        // 防止分母为0出现NoN  
        if (baiz > 0) {  
            double fen = (baiy / baiz) * 100;  
//            //NumberFormat nf = NumberFormat.getPercentInstance();  
//            //nf.setMinimumFractionDigits(2); //保留到小数点后几位  
//            // 百分比格式，后面不足2位的用0补齐  
//            //baifenbi = nf.format(fen);  
//            //注释掉的也是一种方法  
//            DecimalFormat df1 = new DecimalFormat("0");//0.00  
//            baifenbi = df1.format(fen);  

            baifenbi = (int)fen;
        }  
        return baifenbi;  
    }  
  
  
//    private String getFileNameFromUrl(String url) {  
//        if (!TextUtils.isEmpty(url)) {  
//            return url.substring(url.lastIndexOf("/") + 1);  
//        }  
//        return System.currentTimeMillis() + "";  
//    }  
  
//    private void close(Closeable closeable) {  
//        try {  
//            closeable.close();  
//        } catch (IOException e) {
//            Log.e(TAG, e.getMessage(), e);
//        }  
//    }  
  
  
    public void setClient(OkHttpClient mClient) {  
        this.mClient = mClient;  
    }  
  
    public Builder getBuilder() {  
        return mBuilder;  
    }  
  
    public void setBuilder(Builder builder) {  
        this.mBuilder = builder;  
    }  
  
    public String getId() {  
        if (!TextUtils.isEmpty(id)) {  
        } else {  
            id = url;  
        }  
        return id;  
    }  
  
    public String getUrl() {
        return url;
    }

    public String getFileName() {
        return fileName;
    }


    public void setUploadStatus(int uploadStatus) {
        this.uploadStatus = uploadStatus;
    }

    public int getUploadStatus() {
        return uploadStatus;
    }
  
  
    public void setmListener(BigFileUploadTaskListener mListener) {  
        this.mListener = mListener;  
    }

    public int getChunck() {
        return chunck;
    }

    public int getChuncks() {
        return chuncks;
    }

    public static class Builder {

        // task id  
        private String id;
        // file upload url  
        private String url;
        // File name when saving  
        private String fileName; 
        // 文件数据绝对路径
        private String filePath;
        // 总文件的md5码
        private String fileMd5;
        // 上传状态
        private int uploadStatus = UPLOAD_STATUS_INIT;
        // 当前是第几块（注意：基数是从1开始）  
        private int chunck;
        // 上传结果监听者
        private BigFileUploadTaskListener listener;  
        // 用户要额外上传的参数
        private HashMap<String, String> userPropeties;
  
        /** 
         * 作为上传task开始、删除、停止的key值，如果为空则默认是url
         * 
         * @param id 
         * @return 
         */  
        public Builder setId(String id) {  
            this.id = id;  
            return this;  
        }  
  
        /** 
         * 上传url（not null） 
         * 
         * @param url 
         * @return 
         */  
        public Builder setUrl(String url) {  
            this.url = url;  
            return this;  
        }  
  
        /** 
         * 设置上传状态 
         * 
         * @param uploadStatus 
         * @return 
         */  
        public Builder setUploadStatus(int uploadStatus) {  
            this.uploadStatus = uploadStatus;  
            return this;  
        }  
  
        /** 
         * 第几块 
         * 
         * @param chunck 
         * @return 
         */  
        public Builder setChunck(int chunck) {  
            this.chunck = chunck;  
            return this;  
        }  
  
  
        /** 
         * 设置文件名 
         * 
         * @param fileName 
         * @return 
         */  
        public Builder setFileName(String fileName) {  
            this.fileName = fileName;  
            return this;  
        }

        public Builder setFilePath(String filePath) {
            this.filePath = filePath;
            return this;
        }

        /** 
         * 设置上传回调 
         * 
         * @param listener 
         * @return 
         */  
        public Builder setListener(BigFileUploadTaskListener listener) {  
            this.listener = listener;  
            return this;  
        }

        public Builder setUserPropeties(HashMap<String, String> userPropeties) {
            this.userPropeties = userPropeties;
            return this;
        }

        public Builder setFileMd5(String fileMd5) {
            this.fileMd5 = fileMd5;
            return this;
        }

        public BigFileUploadTask build() {  
            return new BigFileUploadTask(this);  
        }  
    }  
  
    private void addParams(MultipartBody.Builder builder, Map<String, String> params) {  
        if (params != null && !params.isEmpty()) {  
            for (String key : params.keySet()) {  
                builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""),  
                        // 使用MediaType.parse("text/plain; charset=utf-8")解决中文乱码问题，否则可以直接用null
                        RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), params.get(key)));  
            }  
        }  
    }


    private static byte[] readBlockFromFile(long offset, File file, int blockSize) {
        byte[] result = new byte[blockSize];
        RandomAccessFile accessFile = null;

        try {
            accessFile = new RandomAccessFile(file, "r");
            accessFile.seek(offset);
            int readSize = accessFile.read(result);
            if (readSize == -1) {
                return null;
            } else if (readSize == blockSize) {
                return result;
            } else {
                byte[] tmpByte = new byte[readSize];
                System.arraycopy(result, 0, tmpByte, 0, readSize);
                return tmpByte;
            }

        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e);
        } catch (OutOfMemoryError ooe){
            Log.e(TAG, ooe.getMessage(), ooe);
        } finally {
            if (accessFile != null) {
                try {
                    accessFile.close();
                } catch (Exception e1) {
                    Log.e(TAG, e1.getMessage(), e1);
                }
            }
        }
        return null;
    }
} 